from __future__ import annotations

from pathlib import Path
import os
import zlib
from typing import Dict, List, Optional, Tuple

import numpy as np
import colorsys
from PySide2.QtCore import Qt, QEvent
from PySide2.QtGui import QColor, QWheelEvent, QMouseEvent
from PySide2.QtWidgets import (
    QAbstractItemView,
    QColorDialog,
    QFileDialog,
    QComboBox,
    QFrame,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QSlider,
    QSplitter,
    QStatusBar,
    QTableWidget,
    QTableWidgetItem,
    QVBoxLayout,
    QWidget,
    QHeaderView,
)
from gglobal import logger
from .language_manager import LanguageManager
try:
    from vispy import scene, app
    _VISPY_AVAILABLE = True
    try:
        app.use_app('pyside2')
    except Exception:
        pass
except Exception as _vp_exc:
    _VISPY_AVAILABLE = False
    scene = None  # type: ignore
    app = None  # type: ignore
    try:
        logger.error("VisPy 导入失败: %s", _vp_exc)
    except Exception:
        pass


class PCDLoader:
    MAX_POINTS: int = 2_000_000

    @staticmethod
    def load_pcd(file_path: str) -> List[Tuple[float, float, float]]:
        p = Path(file_path)
        if not p.exists() or p.suffix.lower() != ".pcd":
            raise ValueError("文件不存在或后缀非 .pcd")
        data_type: Optional[str] = None
        fields: List[str] = []
        sizes: List[int] = []
        types_: List[str] = []
        counts: List[int] = []
        width: int = 0
        height: int = 1
        points_declared: Optional[int] = None
        header_end: int = 0
        with p.open("rb") as f:
            header_lines: List[str] = []
            while True:
                bline = f.readline()
                if not bline:
                    break
                sline = bline.decode("utf-8", errors="replace").strip()
                if not sline or sline.startswith("#"):
                    continue
                header_lines.append(sline)
                if sline.upper().startswith("DATA"):
                    parts = sline.split()
                    data_type = parts[1].lower() if len(parts) >= 2 else None
                    header_end = f.tell()
                    break
        for ln in header_lines:
            up = ln.upper()
            if up.startswith("FIELDS"):
                fields = ln.split()[1:]
            elif up.startswith("SIZE"):
                try:
                    sizes = [int(x) for x in ln.split()[1:]]
                except Exception:
                    sizes = []
            elif up.startswith("TYPE"):
                types_ = ln.split()[1:]
            elif up.startswith("COUNT"):
                try:
                    counts = [int(x) for x in ln.split()[1:]]
                except Exception:
                    counts = []
            elif up.startswith("WIDTH"):
                try:
                    width = int(ln.split()[1])
                except Exception:
                    width = 0
            elif up.startswith("HEIGHT"):
                try:
                    height = int(ln.split()[1])
                except Exception:
                    height = 1
            elif up.startswith("POINTS"):
                try:
                    points_declared = int(ln.split()[1])
                except Exception:
                    points_declared = None
        if not fields:
            raise ValueError("PCD FIELDS 缺失")
        if not counts or len(counts) != len(fields):
            counts = [1] * len(fields)
        if not sizes or len(sizes) != len(fields):
            sizes = [4] * len(fields)
        if not types_ or len(types_) != len(fields):
            types_ = ["F"] * len(fields)
        def idx(name: str) -> int:
            try:
                return fields.index(name)
            except ValueError:
                return -1
        xi, yi, zi = idx("x"), idx("y"), idx("z")
        if min(xi, yi, zi) < 0:
            raise ValueError("PCD FIELDS 中未包含 x/y/z")
        def map_dtype(t: str, s: int) -> str:
            t = t.upper()
            if t == "F":
                return "<f4" if s == 4 else "<f8"
            if t == "I":
                return {1: "<i1", 2: "<i2", 4: "<i4", 8: "<i8"}.get(s, "<i4")
            if t == "U":
                return {1: "<u1", 2: "<u2", 4: "<u4", 8: "<u8"}.get(s, "<u4")
            return "<f4"
        dtype_fields = []
        for i in range(len(fields)):
            dt = np.dtype(map_dtype(types_[i], sizes[i]))
            c = int(counts[i]) if i < len(counts) else 1
            if c == 1:
                dtype_fields.append((fields[i], dt))
            else:
                dtype_fields.append((fields[i], dt, c))
        rec_dtype = np.dtype(dtype_fields)
        if data_type == "ascii":
            points: List[Tuple[float, float, float]] = []
            with p.open("r", encoding="utf-8", errors="replace") as tf:
                header_done = False
                for raw in tf:
                    line = raw.strip()
                    if not line or line.startswith("#"):
                        continue
                    if not header_done:
                        if line.upper().startswith("DATA"):
                            header_done = True
                        continue
                    cols = line.split()
                    if not cols:
                        continue
                    try:
                        x = float(cols[xi])
                        y = float(cols[yi])
                        z = float(cols[zi])
                    except Exception:
                        continue
                    points.append((x, y, z))
                    if len(points) >= PCDLoader.MAX_POINTS:
                        break
            if not points:
                raise ValueError("未解析到有效点数据")
            return points
        if data_type in ("binary", "binary_compressed"):
            with p.open("rb") as bf:
                bf.seek(header_end)
                data = bf.read()
            if data_type == "binary_compressed":
                if len(data) < 8:
                    raise ValueError("binary_compressed 数据不足")
                try:
                    c_size = int.from_bytes(data[0:4], "little", signed=False)
                    u_size = int.from_bytes(data[4:8], "little", signed=False)
                except Exception:
                    raise ValueError("binary_compressed 头解析失败")
                if 8 + c_size > len(data):
                    raise ValueError("binary_compressed 尺寸不匹配")
                comp = data[8:8 + c_size]
                try:
                    decompressed = zlib.decompress(comp)
                except Exception:
                    raise ValueError("binary_compressed 解压失败")
                if u_size and len(decompressed) != u_size:
                    raise ValueError("binary_compressed 解压后尺寸不匹配")
                data_buf = decompressed
            else:
                data_buf = data
            try:
                recs = np.frombuffer(data_buf, dtype=rec_dtype)
            except Exception:
                raise ValueError("二进制数据解析失败")
            if points_declared and recs.shape[0] > points_declared:
                recs = recs[:points_declared]
            if recs.shape[0] == 0:
                raise ValueError("未解析到有效点数据")
            def get_field(arr: np.ndarray, name: str) -> np.ndarray:
                try:
                    v = arr[name]
                    if np.ndim(v) == 2:
                        v = v[:, 0]
                    return np.asarray(v, dtype=np.float32)
                except Exception:
                    return np.zeros(arr.shape[0], dtype=np.float32)
            xs = get_field(recs, fields[xi])
            ys = get_field(recs, fields[yi])
            zs = get_field(recs, fields[zi])
            pts_arr = np.stack([xs, ys, zs], axis=1).astype(np.float32, copy=False)
            return [(float(x), float(y), float(z)) for x, y, z in pts_arr]
        raise ValueError("未知的 PCD DATA 类型")


def _qcolor_to_rgba(color: QColor) -> Tuple[float, float, float, float]:
    return (color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0, 1.0)


class VispyPointCloudEntry:
    def __init__(self, view: "scene.ViewBox", points: List[Tuple[float, float, float]], color: QColor, size: float = 3.0) -> None:
        arr = np.asarray(points, dtype=np.float32)
        if arr.ndim != 2 or arr.shape[1] < 3:
            arr = arr.reshape(-1, 3)
        finite_mask = np.isfinite(arr).all(axis=1)
        arr = arr[finite_mask]
        self.points = np.ascontiguousarray(arr[:, :3], dtype=np.float32)
        self._base_color = QColor(color)
        self._size = float(size)
        self._mode = "uniform"
        self.markers = scene.visuals.Markers(parent=view.scene) if _VISPY_AVAILABLE else None
        self._apply_uniform_colors()

    def _apply_uniform_colors(self) -> None:
        if not _VISPY_AVAILABLE or self.markers is None:
            return
        rgba = _qcolor_to_rgba(self._base_color)
        self.markers.set_data(pos=self.points, face_color=rgba, size=self._size)

    def _apply_z_colormap(self) -> None:
        if not _VISPY_AVAILABLE or self.markers is None:
            return
        z = self.points[:, 2]
        if z.size == 0:
            self._apply_uniform_colors()
            return
        z_min = float(z.min())
        z_max = float(z.max())
        if z_max - z_min < 1e-12:
            self._apply_uniform_colors()
            return
        norm = (z - z_min) / (z_max - z_min)
        r = norm
        g = 1.0 - np.clip(norm - 0.5, 0.0, 1.0)
        b = 1.0 - norm
        a = np.ones_like(norm)
        face = np.stack([r, g, b, a], axis=1)
        self.markers.set_data(pos=self.points, face_color=face, size=self._size)

    def set_visible(self, visible: bool) -> None:
        if not _VISPY_AVAILABLE or self.markers is None:
            return
        self.markers.visible = bool(visible)

    def set_color(self, color: QColor) -> None:
        self._base_color = QColor(color)
        if self._mode == "uniform":
            self._apply_uniform_colors()
        else:
            self._apply_z_colormap()

    def set_size(self, size: float) -> None:
        self._size = float(max(1.0, size))
        if self._mode == "uniform":
            self._apply_uniform_colors()
        else:
            self._apply_z_colormap()

    def apply_color_map(self, mode: str) -> None:
        self._mode = mode
        if mode == "map_z":
            self._apply_z_colormap()
        else:
            self._apply_uniform_colors()


class VispyGraphPanel:
    def __init__(self, parent: Optional[QWidget] = None) -> None:
        self._parent = parent
        self.canvas = None
        self.view = None
        self.grid = None
        if _VISPY_AVAILABLE:
            try:
                # 嵌入式画布：禁用独立窗口显示，使用原生Qt控件进行嵌入
                self.canvas = scene.SceneCanvas(keys=None, bgcolor="#111111", size=(800, 600), show=False)
                native = self.canvas.native  # type: ignore
                try:
                    # 使用 StrongFocus 以确保点击即可获得焦点（左键旋转生效）
                    native.setFocusPolicy(Qt.StrongFocus)
                    native.setMouseTracking(True)
                    native.setFocus()
                except Exception:
                    pass
                self.view = self.canvas.central_widget.add_view()
                self.view.camera = 'turntable'
                scene.visuals.XYZAxis(parent=self.view.scene)
                self._init_grid(size=100.0, spacing=10.0)
                try:
                    logger.info("VisPy 画布创建成功，已初始化坐标轴与栅格")
                except Exception:
                    pass
                # 强制设置 turntable 相机参数，确保中键平移生效
                try:
                    cam = self.view.camera
                    cam.fov = 45.0
                    cam.distance = 100.0
                    logger.info("VisPy 相机强制设置为 turntable，fov=45，distance=100")
                except Exception:
                    pass
                try:
                    self._panning = False
                    self._pan_last = None
                    self.canvas.events.mouse_press.connect(self._on_mouse_press)
                    self.canvas.events.mouse_move.connect(self._on_mouse_move)
                    self.canvas.events.mouse_release.connect(self._on_mouse_release)
                except Exception:
                    pass
            except Exception as exc:
                try:
                    logger.error("创建 VisPy 画布失败: %s", exc)
                except Exception:
                    pass
                self.canvas = None
                self.view = None

    def create_container(self, parent: QWidget) -> QWidget:
        if not _VISPY_AVAILABLE or self.canvas is None:
            msg = QLabel("VisPy 未安装或不可用，请在 conda base 环境中安装：conda install -c conda-forge vispy", parent)
            msg.setAlignment(Qt.AlignCenter)
            try:
                logger.warning("VisPy 不可用，返回占位提示控件")
            except Exception:
                pass
            return msg
        try:
            logger.info("返回 VisPy 原生容器以嵌入到右侧面板")
        except Exception:
            pass
        # 返回原生Qt控件用于嵌入布局
        return self.canvas.native  # type: ignore[attr-defined]

    def add_entry(self, points: List[Tuple[float, float, float]], color: QColor, size: float = 3.0) -> VispyPointCloudEntry:
        if self.view is None:
            raise RuntimeError("VisPy 视图未初始化")
        try:
            entry = VispyPointCloudEntry(self.view, points, color, size=size)
            try:
                logger.info("VisPy 条目创建成功: 点数=%d, 点大小=%.2f", len(points), float(size))
            except Exception:
                pass
            return entry
        except Exception as exc:
            try:
                logger.exception("创建 VisPy 条目异常: %s", exc)
            except Exception:
                pass
            raise

    def set_view_mode(self, mode: str) -> None:
        if self.view is None:
            return
        # 确保为 turntable 相机
        try:
            self.view.camera = 'turntable'
        except Exception:
            self.view.camera = scene.cameras.TurntableCamera()
        cam = self.view.camera
        if mode == "主视图":
            cam.fov = 45.0
            cam.elevation = 30.0
            cam.azimuth = 45.0
            try:
                cam.distance = 100.0
            except Exception:
                pass
        elif mode == "俯视图":
            cam.fov = 45.0
            cam.elevation = 90.0
            cam.azimuth = 0.0
            try:
                cam.distance = 100.0
            except Exception:
                pass
        else:
            cam.fov = 45.0
            cam.elevation = 0.0
            cam.azimuth = 90.0
            try:
                cam.distance = 100.0
            except Exception:
                pass
        try:
            logger.info("相机视图模式已应用: %s (fov=%.1f, elev=%.1f, azim=%.1f)", mode, float(cam.fov), float(cam.elevation), float(cam.azimuth))
        except Exception:
            pass

    def update_view_range(self, entries: List[VispyPointCloudEntry]) -> None:
        try:
            if self.view is None or not entries:
                return
            mins = []
            maxs = []
            for e in entries:
                pts = e.points
                mins.append(pts.min(axis=0))
                maxs.append(pts.max(axis=0))
            mn = np.min(np.stack(mins, axis=0), axis=0)
            mx = np.max(np.stack(maxs, axis=0), axis=0)
            self._mn = mn
            self._mx = mx
            self.view.camera.set_range(x=(float(mn[0]), float(mx[0])), y=(float(mn[1]), float(mx[1])), z=(float(mn[2]), float(mx[2])))
            self._update_grid_extent(mn, mx)
            try:
                logger.info("视图范围更新: x[%.3f,%.3f] y[%.3f,%.3f] z[%.3f,%.3f]", float(mn[0]), float(mx[0]), float(mn[1]), float(mx[1]), float(mn[2]), float(mx[2]))
            except Exception:
                pass
        except Exception as exc:
            try:
                logger.exception("更新视图范围异常: %s", exc)
            except Exception:
                pass

    def _init_grid(self, size: float = 100.0, spacing: float = 10.0) -> None:
        try:
            if self.view is None:
                return
            half = float(size) / 2.0
            nx = max(1, int(np.ceil(size / spacing)))
            xs = np.linspace(-half, half, nx + 1)
            ys = np.linspace(-half, half, nx + 1)
            segs = []
            for y in ys:
                segs.append([-half, float(y), 0.0])
                segs.append([half, float(y), 0.0])
            for x in xs:
                segs.append([float(x), -half, 0.0])
                segs.append([float(x), half, 0.0])
            pos = np.array(segs, dtype=np.float32)
            self.grid = scene.visuals.Line(pos=pos, color=(0.6, 0.6, 0.6, 0.4), method='gl', connect='segments', parent=self.view.scene)
            try:
                logger.info("初始栅格创建完成: size=%.1f, spacing=%.1f", float(size), float(spacing))
            except Exception:
                pass
        except Exception as exc:
            try:
                logger.exception("创建初始栅格异常: %s", exc)
            except Exception:
                pass

    def _update_grid_extent(self, mn: np.ndarray, mx: np.ndarray) -> None:
        try:
            if self.view is None or self.grid is None:
                return
            size_x = float(max(1.0, mx[0] - mn[0]))
            size_y = float(max(1.0, mx[1] - mn[1]))
            size = float(max(size_x, size_y))
            spacing = max(1.0, size / 20.0)
            half = size / 2.0
            nx = max(1, int(np.ceil(size / spacing)))
            xs = np.linspace(-half, half, nx + 1)
            ys = np.linspace(-half, half, nx + 1)
            segs = []
            for y in ys:
                segs.append([-half, float(y), 0.0])
                segs.append([half, float(y), 0.0])
            for x in xs:
                segs.append([float(x), -half, 0.0])
                segs.append([float(x), half, 0.0])
            pos = np.array(segs, dtype=np.float32)
            self.grid.set_data(pos=pos)
        except Exception as exc:
            try:
                logger.exception("更新栅格范围异常: %s", exc)
            except Exception:
                pass

    def _on_mouse_press(self, event) -> None:
        try:
            btn = getattr(event, "button", None)
            mods = getattr(event, "modifiers", ())
            if btn == 3 or (btn == 1 and (mods and ("Shift" in mods or "shift" in mods))):
                self._panning = True
                self._pan_last = tuple(event.pos)
                try:
                    event.handled = True
                except Exception:
                    pass
        except Exception:
            pass

    def _on_mouse_move(self, event) -> None:
        try:
            if not self._panning or self.view is None or self._pan_last is None:
                return
            w, h = self.canvas.size
            dx = float(event.pos[0] - self._pan_last[0])
            dy = float(event.pos[1] - self._pan_last[1])
            rng = getattr(self, "_mx", None)
            rng2 = getattr(self, "_mn", None)
            if rng is not None and rng2 is not None:
                rx = float(rng[0] - rng2[0])
                ry = float(rng[1] - rng2[1])
            else:
                rx = 100.0
                ry = 100.0
            sx = -dx / max(1.0, float(w)) * rx
            sy = dy / max(1.0, float(h)) * ry
            cam = self.view.camera
            try:
                cx, cy, cz = cam.center
            except Exception:
                cx, cy, cz = 0.0, 0.0, 0.0
            cam.center = (cx + sx, cy + sy, cz)
            self._pan_last = tuple(event.pos)
            try:
                event.handled = True
            except Exception:
                pass
        except Exception:
            pass

    def _on_mouse_release(self, event) -> None:
        try:
            btn = getattr(event, "button", None)
            mods = getattr(event, "modifiers", ())
            if btn == 3 or (btn == 1 and (mods and ("Shift" in mods or "shift" in mods))):
                self._panning = False
                self._pan_last = None
                try:
                    event.handled = True
                except Exception:
                    pass
        except Exception:
            pass

    # 已移除自定义平移事件，使用 VisPy 'turntable' 相机的默认交互：
    # 左键旋转、中键平移、滚轮缩放


class PCDViewerWindow(QMainWindow):
    def __init__(self, parent: Optional[QWidget] = None) -> None:
        super().__init__(parent)
        self.language_manager = LanguageManager()
        self.setWindowTitle(self.language_manager.get_text("pcd_viewer_window_title", "PCD 查看器"))
        self.setMinimumSize(900, 600)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        splitter = QSplitter(Qt.Horizontal, self)
        self.setCentralWidget(splitter)
        left_panel = QFrame(self)
        left_panel.setMinimumWidth(320)
        left_layout = QVBoxLayout(left_panel)
        left_layout.setContentsMargins(8, 8, 8, 8)
        left_layout.setSpacing(8)
        self.btn_add = QPushButton(self.language_manager.get_text("pcd_viewer_add_button", "添加文件"), left_panel)
        left_layout.addWidget(self.btn_add)
        self.lbl_size = QLabel(self.language_manager.get_text("pcd_viewer_point_size_label", "点大小"), left_panel)
        self.size_slider = QSlider(Qt.Horizontal, left_panel)
        self.size_slider.setRange(1, 10)
        self.size_slider.setValue(4)
        left_layout.addWidget(self.lbl_size)
        left_layout.addWidget(self.size_slider)
        self.lbl_cmap = QLabel(self.language_manager.get_text("pcd_viewer_color_map_label", "颜色映射"), left_panel)
        self.color_map_combo = QComboBox(left_panel)
        self.language_manager.register_combo_box(self.color_map_combo, "pcd_viewer_color_map_items")
        left_layout.addWidget(self.lbl_cmap)
        left_layout.addWidget(self.color_map_combo)
        # 渲染引擎（仅 VisPy）
        self.lbl_mode = QLabel(self.language_manager.get_text("pcd_viewer_mode_label", "加载模式"), left_panel)
        self.mode_combo = QComboBox(left_panel)
        self.mode_combo.clear()
        self.mode_combo.addItems(["VisPy"])
        self.mode_combo.setEnabled(False)
        left_layout.addWidget(self.lbl_mode)
        left_layout.addWidget(self.mode_combo)
        self.lbl_view = QLabel("视图模式", left_panel)
        self.view_combo = QComboBox(left_panel)
        self.view_combo.addItems(["主视图", "俯视图", "侧视图"])
        left_layout.addWidget(self.lbl_view)
        left_layout.addWidget(self.view_combo)
        self.table = QTableWidget(left_panel)
        self.table.setColumnCount(3)
        headers = self.language_manager.get_text("pcd_viewer_table_headers", ["显示", "文件名", "颜色"])
        try:
            if isinstance(headers, list) and len(headers) == 3:
                self.table.setHorizontalHeaderLabels(headers)
            else:
                self.table.setHorizontalHeaderLabels(["显示", "文件名", "颜色"])
        except Exception:
            self.table.setHorizontalHeaderLabels(["显示", "文件名", "颜色"])
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
        self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)
        left_layout.addWidget(self.table, stretch=1)
        splitter.addWidget(left_panel)
        # 右侧视图区域（根据加载模式创建）
        right_panel = QFrame(self)
        right_layout = QHBoxLayout(right_panel)
        right_layout.setContentsMargins(0, 0, 0, 0)
        right_layout.setSpacing(0)
        self._render_mode = "VisPy"
        self.mode_combo.setCurrentIndex(0)
        self._right_panel = right_panel
        self._right_layout = right_layout
        self._backend_widget: Optional[QWidget] = None
        self._vispy_panel: Optional[VispyGraphPanel] = None
        self._view_mode: str = "主视图"
        self._row_entries: Dict[int, object] = {}
        self._row_to_path: Dict[int, str] = {}
        self._row_to_points: Dict[int, List[Tuple[float, float, float]]] = {}
        self._colors: List[QColor] = [
            QColor(255, 99, 71),
            QColor(30, 144, 255),
            QColor(60, 179, 113),
            QColor(218, 165, 32),
            QColor(123, 104, 238),
            QColor(255, 105, 180),
            QColor(0, 191, 255),
        ]
        self._color_idx = 0
        self._projection = "XY"
        self._build_right_panel()
        splitter.addWidget(right_panel)
        splitter.setStretchFactor(0, 0)
        splitter.setStretchFactor(1, 1)
        self.status = QStatusBar(self)
        self.setStatusBar(self.status)
        # 事件过滤器平移状态
        self._ef_panning: bool = False
        self._ef_last_pos: Optional[Tuple[int, int]] = None
        self.btn_add.clicked.connect(self._on_add_files_clicked)
        self.table.itemChanged.connect(self._on_table_item_changed)
        self.table.cellClicked.connect(self._on_table_cell_clicked)
        self.size_slider.valueChanged.connect(self._on_point_size_changed)
        self.color_map_combo.currentIndexChanged.connect(self._on_color_map_changed)
        self.view_combo.currentIndexChanged.connect(self._on_view_changed)
        self.mode_combo.currentIndexChanged.connect(self._on_mode_changed)
        try:
            self.language_manager.register_widget(self, "pcd_viewer_window_title", property_name="windowTitle")
            self.language_manager.register_widget(self.btn_add, "pcd_viewer_add_button")
            self.language_manager.register_widget(self.lbl_size, "pcd_viewer_point_size_label")
            self.language_manager.register_widget(self.lbl_cmap, "pcd_viewer_color_map_label")
        except Exception:
            pass
        try:
            self.language_manager.language_changed.connect(self._on_language_changed)
        except Exception:
            pass

    def _resolve_default_download_dir(self) -> Path:
        try:
            home = Path.home()
            candidates = [home / "Downloads", home / "下载"]
            for c in candidates:
                try:
                    c.mkdir(parents=True, exist_ok=True)
                    return c
                except Exception:
                    pass
        except Exception:
            pass
        fallback = Path("resource/pcd_download").resolve()
        try:
            fallback.mkdir(parents=True, exist_ok=True)
        except Exception:
            pass
        return fallback

    def _pick_bright_color(self) -> QColor:
        h = float(np.random.rand())
        s = 1.0
        v = 1.0
        r, g, b = colorsys.hsv_to_rgb(h, s, v)
        return QColor(int(r * 255), int(g * 255), int(b * 255))

    def _build_right_panel(self) -> None:
        try:
            # 清理旧面板
            if self._backend_widget is not None:
                self._right_layout.removeWidget(self._backend_widget)
                self._backend_widget.setParent(None)
                self._backend_widget = None
            self._vispy_panel = None
            self._render_mode = self.mode_combo.currentText()
            panel = VispyGraphPanel(self._right_panel)
            container = panel.create_container(self._right_panel)
            self._backend_widget = container
            self._vispy_panel = panel if _VISPY_AVAILABLE and panel.view is not None else None
            self._right_layout.addWidget(container)
            try:
                # 安装事件过滤器以强化滚轮缩放在打包环境中的可用性
                container.installEventFilter(self)
            except Exception:
                pass
            self.color_map_combo.setEnabled(True)
            self.lbl_view.show()
            self.view_combo.show()
            try:
                logger.info("右侧面板已创建: 使用渲染模式=%s, VisPy可用=%s", self._render_mode, str(self._vispy_panel is not None))
            except Exception:
                pass
            # 重建现有条目
            if self._row_entries:
                # 重新加载并绘制，保持表格不变
                for row, pts in list(self._row_to_points.items()):
                    color_item = self.table.item(row, 2)
                    color = color_item.background().color() if color_item is not None else QColor(255, 255, 255)
                    size = float(self.size_slider.value())
                    if self._vispy_panel is not None:
                        entry = self._vispy_panel.add_entry(pts, color, size)
                    else:
                        continue
                    self._row_entries[row] = entry
                # 更新范围
                if self._vispy_panel is not None:
                    vp_entries = [e for e in self._row_entries.values() if isinstance(e, VispyPointCloudEntry)]
                    self._vispy_panel.update_view_range(vp_entries)
                    self._vispy_panel.set_view_mode(getattr(self, "_view_mode", "主视图"))
        except Exception as exc:
            try:
                logger.exception("构建右侧面板异常: %s", exc)
            except Exception:
                pass

    def _get_scene_bounds(self) -> Optional[Tuple[np.ndarray, np.ndarray]]:
        try:
            if not self._row_entries:
                return None
            mins = []
            maxs = []
            for entry in self._row_entries.values():
                pts = entry.points
                mins.append(pts.min(axis=0))
                maxs.append(pts.max(axis=0))
            if not mins or not maxs:
                return None
            mn = np.min(np.stack(mins, axis=0), axis=0)
            mx = np.max(np.stack(maxs, axis=0), axis=0)
            return mn, mx
        except Exception:
            return None

    def _update_view_range(self) -> None:
        try:
            if self._vispy_panel is None:
                return
            vp_entries = [e for e in self._row_entries.values() if isinstance(e, VispyPointCloudEntry)]
            self._vispy_panel.update_view_range(vp_entries)
        except Exception:
            pass

    def _on_add_files_clicked(self) -> None:
        try:
            start_dir = str(self._resolve_default_download_dir())
            dlg_title = self.language_manager.get_constant("pcd_viewer_file_dialog_title") or self.language_manager.get_text("pcd_viewer_file_dialog_title", "选择 PCD 文件")
            file_filter = self.language_manager.get_constant("pcd_viewer_file_filter") or self.language_manager.get_text("pcd_viewer_file_filter", "PCD 文件 (*.pcd)")
            paths, _ = QFileDialog.getOpenFileNames(self, dlg_title, start_dir, file_filter)
            if not paths:
                return
            try:
                logger.info("选择PCD文件: %d 个", len(paths))
            except Exception:
                pass
            self.add_files(paths)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_add_files_failed") or "添加文件失败：{error}"
            self.status.showMessage(err_format.format(error=exc), 5000)

    def add_files(self, paths: List[str]) -> None:
        try:
            if not paths:
                return
            added = 0
            for p in paths:
                if self._add_file_internal(p):
                    added += 1
            add_ok_format = self.language_manager.get_constant("pcd_viewer_add_files_success") or "已添加 {count} 个文件"
            self.status.showMessage(add_ok_format.format(count=added), 3000)
            try:
                logger.info("文件添加完成: 目标=%d, 成功=%d", len(paths), added)
            except Exception:
                pass
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_add_files_failed") or "添加文件失败：{error}"
            self.status.showMessage(err_format.format(error=exc), 5000)

    def _add_file_internal(self, p: str) -> bool:
        try:
            pts = PCDLoader.load_pcd(p)
        except Exception as exc:
            err_format = self.language_manager.get_constant("pcd_viewer_parse_failed") or "文件解析失败：{name} - {error}"
            try:
                self.status.showMessage(err_format.format(name=Path(p).name, error=exc), 5000)
            except Exception:
                pass
            logger.exception("解析 PCD 失败: %s", exc)
            return False
        try:
            logger.info("加载PCD成功: 文件=%s, 点数=%d", Path(p).name, len(pts))
        except Exception:
            pass
        try:
            color = self._pick_bright_color()
        except Exception:
            color = QColor(255, 255, 0)
        if self._vispy_panel is not None:
            try:
                entry = self._vispy_panel.add_entry(
                    pts,
                    color,
                    size=float(self.size_slider.value()),
                )
            except Exception as exc:
                err_format = self.language_manager.get_constant("pcd_viewer_add_gl_item_failed") or "添加绘制项失败：{error}"
                try:
                    self.status.showMessage(err_format.format(error=exc), 5000)
                except Exception:
                    pass
                logger.exception("添加绘制项失败: %s", exc)
                return False
        else:
            return False
        try:
            row = self.table.rowCount()
            self.table.insertRow(row)
            chk = QTableWidgetItem("")
            chk.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            chk.setCheckState(Qt.Checked)
            self.table.setItem(row, 0, chk)
            full_path = str(Path(p))
            name_item = QTableWidgetItem(Path(p).name)
            name_item.setToolTip(full_path)
            self.table.setItem(row, 1, name_item)
            color_item = QTableWidgetItem("")
            color_item.setFlags(Qt.ItemIsEnabled)
            color_item.setBackground(color)
            color_item.setToolTip(self.language_manager.get_text("pcd_viewer_color_cell_tooltip", "点击以修改点云颜色"))
            self.table.setItem(row, 2, color_item)
            self._row_entries[row] = entry
            self._row_to_path[row] = full_path
            self._row_to_points[row] = pts
            self._update_view_range()
            try:
                logger.info("表格更新成功: 行=%d, 文件=%s", row, Path(p).name)
            except Exception:
                pass
        except Exception as exc:
            logger.exception("更新 PCDViewer 表格失败: %s", exc)
            return False
        return True

    def eventFilter(self, obj, event):
        """事件过滤器：增强打包环境下的滚轮缩放交互"""
        try:
            if obj is self._backend_widget and event is not None:
                et = int(event.type())
                # 处理滚轮事件：根据滚轮增量调整相机距离
                if et == int(QEvent.Wheel) and self._vispy_panel is not None and self._vispy_panel.view is not None:
                    try:
                        we = event  # type: QWheelEvent
                        delta = 0
                        try:
                            # Qt5: angleDelta().y() 返回 120 的倍数
                            delta = int(we.angleDelta().y())
                        except Exception:
                            delta = 0
                        cam = self._vispy_panel.view.camera
                        # 以百分比方式缩放距离，避免过度跳变
                        factor = 1.0 - float(delta) / 120.0 * 0.1
                        # 限制缩放因子范围
                        factor = float(max(0.2, min(5.0, factor)))
                        try:
                            dist = float(getattr(cam, 'distance', 100.0))
                            setattr(cam, 'distance', float(max(1.0, min(1e6, dist * factor))))
                        except Exception:
                            pass
                        return False  # 继续交由默认处理，以兼容内置交互
                    except Exception:
                        return False
                # 处理鼠标平移：右键或 Shift+左键
                if et == int(QEvent.MouseButtonPress) and self._vispy_panel is not None and self._vispy_panel.view is not None:
                    try:
                        me = event  # type: QMouseEvent
                        btn = int(me.button())
                        mods = int(me.modifiers())
                        if btn == Qt.RightButton or (btn == Qt.LeftButton and (mods & Qt.ShiftModifier)):
                            self._ef_panning = True
                            self._ef_last_pos = (int(me.pos().x()), int(me.pos().y()))
                            return False
                    except Exception:
                        pass
                if et == int(QEvent.MouseMove) and self._ef_panning and self._vispy_panel is not None and self._vispy_panel.view is not None and self._ef_last_pos is not None:
                    try:
                        me = event  # type: QMouseEvent
                        nx, ny = int(me.pos().x()), int(me.pos().y())
                        dx = float(nx - self._ef_last_pos[0])
                        dy = float(ny - self._ef_last_pos[1])
                        rng = getattr(self._vispy_panel, "_mx", None)
                        rng2 = getattr(self._vispy_panel, "_mn", None)
                        if rng is not None and rng2 is not None:
                            rx = float(rng[0] - rng2[0])
                            ry = float(rng[1] - rng2[1])
                        else:
                            rx = 100.0
                            ry = 100.0
                        # 根据窗口尺寸进行归一化平移
                        try:
                            w = float(self._backend_widget.width())
                            h = float(self._backend_widget.height())
                        except Exception:
                            w, h = 800.0, 600.0
                        sx = -dx / max(1.0, w) * rx
                        sy = dy / max(1.0, h) * ry
                        cam = self._vispy_panel.view.camera
                        try:
                            cx, cy, cz = cam.center
                        except Exception:
                            cx, cy, cz = 0.0, 0.0, 0.0
                        cam.center = (cx + sx, cy + sy, cz)
                        self._ef_last_pos = (nx, ny)
                        return False
                    except Exception:
                        pass
                if et == int(QEvent.MouseButtonRelease):
                    self._ef_panning = False
                    self._ef_last_pos = None
        except Exception:
            pass
        return False

    def _on_table_item_changed(self, item: QTableWidgetItem) -> None:
        try:
            if item.column() != 0:
                return
            row = item.row()
            entry = self._row_entries.get(row)
            if entry is None:
                return
            entry.set_visible(item.checkState() == Qt.Checked)
            try:
                logger.info("条目可见性更新: 行=%d, 可见=%s", row, str(item.checkState() == Qt.Checked))
            except Exception:
                pass
        except Exception:
            logger.exception("更新条目可见性异常")

    def _on_table_cell_clicked(self, row: int, column: int) -> None:
        try:
            if column != 2:
                return
            entry = self._row_entries.get(row)
            if entry is None:
                return
            current_bg = self.table.item(row, column)
            initial = current_bg.background().color() if current_bg is not None else QColor(255, 255, 255)
            color_dlg_title = self.language_manager.get_constant("pcd_viewer_color_dialog_title") or self.language_manager.get_text("pcd_viewer_color_dialog_title", "选择点云颜色")
            color = QColorDialog.getColor(initial, self, color_dlg_title)
            if not color.isValid():
                return
            entry.set_color(color)
            cell = self.table.item(row, column)
            if cell is not None:
                cell.setBackground(color)
            self._update_status()
            try:
                logger.info("颜色更新: 行=%d, 新颜色=(%d,%d,%d)", row, int(color.red()), int(color.green()), int(color.blue()))
            except Exception:
                pass
        except Exception:
            logger.exception("更新颜色异常")

    def _on_point_size_changed(self, value: int) -> None:
        try:
            for entry in self._row_entries.values():
                entry.set_size(float(value))
            self._update_status()
            try:
                logger.info("点大小更新: size=%d", int(value))
            except Exception:
                pass
        except Exception:
            logger.exception("更新点大小异常")

    def _on_color_map_changed(self, index: int) -> None:
        try:
            mode = "uniform" if int(index) == 0 else "map_z"
            for entry in self._row_entries.values():
                if isinstance(entry, VispyPointCloudEntry):
                    entry.apply_color_map(mode)
            self._update_status()
            try:
                logger.info("颜色映射模式更新: %s", mode)
            except Exception:
                pass
        except Exception:
            logger.exception("更新颜色映射模式异常")

    def _on_mode_changed(self, _index: int) -> None:
        try:
            # 切换加载模式并重建右侧面板
            self._build_right_panel()
            self._update_status()
            try:
                logger.info("加载模式切换，已重建右侧面板")
            except Exception:
                pass
        except Exception:
            logger.exception("切换加载模式异常")

    def _on_view_changed(self, index: int) -> None:
        try:
            sel = self.view_combo.currentText()
            self._view_mode = sel
            if self._vispy_panel is not None:
                self._vispy_panel.set_view_mode(self._view_mode)
            self._update_view_range()
            self._update_status()
            try:
                logger.info("视图模式切换: %s", sel)
            except Exception:
                pass
        except Exception:
            logger.exception("切换视图模式异常")

    def _update_status(self) -> None:
        try:
            files = len(self._row_entries)
            size = self.size_slider.value()
            fmt = self.language_manager.get_constant("pcd_viewer_status_format") or "文件: {files} | 点大小: {size} | 颜色映射: {mode}"
            self.status.showMessage(fmt.format(files=files, size=size, mode=self.color_map_combo.currentText()))
            try:
                logger.info("状态更新: 文件=%d, 点大小=%d, 映射=%s", int(files), int(size), str(self.color_map_combo.currentText()))
            except Exception:
                pass
        except Exception:
            logger.exception("更新状态栏异常")

    

    def _on_language_changed(self, _lang: dict) -> None:
        try:
            self.setWindowTitle(self.language_manager.get_text("pcd_viewer_window_title", "PCD 查看器"))
            headers = self.language_manager.get_text("pcd_viewer_table_headers", ["显示", "文件名", "颜色"])
            if isinstance(headers, list) and len(headers) == 3:
                self.table.setHorizontalHeaderLabels(headers)
            self._update_status()
            try:
                logger.info("语言已切换，UI文本已更新")
            except Exception:
                pass
        except Exception:
            logger.exception("切换语言异常")


__all__ = [
    "PCDLoader",
    "PCDViewerWindow",
]
